package com.gitee.wsl.compose.data

import com.gitee.wsl.data.tiler.ListTiler
import com.gitee.wsl.data.tiler.PivotRequest
import com.gitee.wsl.data.tiler.Tile
import com.gitee.wsl.data.tiler.TiledList
import com.gitee.wsl.data.tiler.distinct
import com.gitee.wsl.data.tiler.emptyTiledList
import com.gitee.wsl.data.tiler.groupBy
import com.gitee.wsl.data.tiler.listTiler
import com.gitee.wsl.data.tiler.toPivotedTileInputs
import com.gitee.wsl.data.tiler.toTiledList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlin.math.max
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import androidx.compose.ui.graphics.toArgb
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.random.Random

private const val ITEMS_PER_PAGE = 50

private const val MIN_ITEMS_PER_PAGE = 50

val ascendingPageComparator = compareBy(PageQuery::page)
val descendingPageComparator = ascendingPageComparator.reversed()

// Query for items describing the page and sort order
data class PageQuery(
    val page: Int,
    val isAscending: Boolean
)

data class State(
    val isAscending: Boolean = true,
    val itemsPerPage: Int = ITEMS_PER_PAGE,
    val currentPage: Int = 0,
    val firstVisibleIndex: Int = -1,
    val pivotSummary: String,
    val items: TiledList<PageQuery, NumberTile> = emptyTiledList()
)

val State.groupedItems get() = items.groupBy { it.number / ITEMS_PER_PAGE}

class Loader(
    isDark: Boolean,
    scope: CoroutineScope
) {
    // Current query that is visible in the view port
    private val currentQuery = MutableStateFlow(
        PageQuery(
            page = 0,
            isAscending = true
        )
    )

    // Number of columns in the grid
    private val numberOfColumns = MutableStateFlow(1)

    // Flow specifying the pivot configuration
    private val pivotRequests = combine(
        currentQuery.map { it.isAscending },
        numberOfColumns,
        ::pivotRequest
    ).distinctUntilChanged()

    // Define inputs that match the current pivoted position
    private val pivotInputs = currentQuery.toPivotedTileInputs(
        pivotRequests = pivotRequests
    )
        .shareIn(scope, SharingStarted.WhileSubscribed())

    // Allows for changing the order on response to user input
    private val orderInputs = currentQuery
        .map { pageQuery ->
            Tile.Order.PivotSorted<PageQuery, NumberTile>(
                query = pageQuery,
                comparator = when {
                    pageQuery.isAscending -> ascendingPageComparator
                    else -> descendingPageComparator
                }
            )
        }
        .distinctUntilChanged()

    // Change limit to account for dynamic view port size
    private val limitInputs = numberOfColumns.map { noColumns ->
        Tile.Limiter<PageQuery, NumberTile>(
            maxQueries = max(
                a = noColumns * 2,
                b = 3
            ),
            itemSizeHint = ITEMS_PER_PAGE
        )
    }

    private val tiledList = merge(
        pivotInputs,
        orderInputs,
        limitInputs,
    )
        .toTiledList(
            numberTiler(isDark = isDark)
        )
        .filter { it.size >= MIN_ITEMS_PER_PAGE }
        .shareIn(scope, SharingStarted.WhileSubscribed())

    val state = combine(
        pivotInputs.pivotSummaries(),
        currentQuery,
        tiledList,
    ) { pivotSummary, pageQuery, tiledList ->
        State(
            isAscending = pageQuery.isAscending,
            currentPage = pageQuery.page,
            pivotSummary = pivotSummary,
            items = tiledList.distinct()
        )
    }
        .stateIn(
            scope = scope,
            started = SharingStarted.WhileSubscribed(),
            initialValue = State(pivotSummary = "")
        )

    fun setCurrentPage(page: Int) = currentQuery.update { query ->
        query.copy(page = page)
    }

    fun toggleOrder() = currentQuery.update { query ->
        query.copy(isAscending = !query.isAscending)
    }

    fun setNumberOfColumns(numberOfColumns: Int) = this.numberOfColumns.update {
        numberOfColumns
    }

    // Avoid breaking object equality in [PivotRequest] by using vals
    private val nextQuery: PageQuery.() -> PageQuery? = {
        copy(page = page + 1)
    }
    private val previousQuery: PageQuery.() -> PageQuery? = {
        copy(page = page - 1).takeIf { it.page >= 0 }
    }

    /**
     * Pivoted tiling with the grid size as a dynamic input parameter
     */
    private fun pivotRequest(
        isAscending: Boolean,
        numberOfColumns: Int,
    ) = PivotRequest<PageQuery, NumberTile>(
        onCount = max(a = 3, b = 2 * numberOfColumns),
        offCount = 2 * numberOfColumns,
        nextQuery = nextQuery,
        previousQuery = previousQuery,
        comparator = when {
            isAscending -> ascendingPageComparator
            else -> descendingPageComparator
        }
    )
}

val State.tilingSummary
    get() =
        """
Items per page: $itemsPerPage
Tiled list size: ${items.size}
$pivotSummary
""".trim()

/**
 * Fetches a [Map] of [PageQuery] to [NumberTile] where the [NumberTile] instances self update
 */
private fun numberTiler(
    isDark: Boolean,
): ListTiler<PageQuery, NumberTile> =
    listTiler(
        limiter = Tile.Limiter(
            maxQueries = 3,
            itemSizeHint = ITEMS_PER_PAGE
        ),
        order = Tile.Order.PivotSorted(
            query = PageQuery(page = 0, isAscending = true),
            comparator = ascendingPageComparator
        ),
        fetcher = { pageQuery ->
            pageQuery.colorShiftingTiles(ITEMS_PER_PAGE, isDark)
        }
    )

private fun Flow<Tile.Input<PageQuery, NumberTile>>.pivotSummaries(): Flow<String> =
    // Use batch requests as a snapshot of the tiling pipeline
    filterIsInstance<Tile.Batch<PageQuery, NumberTile>>()
        .map { input ->
            listOf(
                "Active pages: ${input.on.map(PageQuery::page).sorted()}",
                "Pages in memory: ${input.off.map(PageQuery::page).sorted()}",
                "Evicted: ${input.evict.map(PageQuery::page).sorted()}"
            ).joinToString(separator = "\n")
        }


data class NumberTile(
    val number: Int,
    val color: Int,
)
val NumberTile.key get() = "tile-$number"

@Composable
fun NumberTile(
    modifier: Modifier = Modifier,
    numberTile: NumberTile
) {
    Button(
        modifier = modifier
            .fillMaxWidth()
            .padding(horizontal = 8.dp),
        //elevation = ButtonDefaults.elevation(defaultElevation = 0.dp),
        border = BorderStroke(width = 2.dp, color = Color(numberTile.color)),
        //colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.surface),
        onClick = { /*TODO*/ },
        content = {
            Text(
                text = numberTile.number.toString(),
                color = Color(numberTile.color)
            )
        }
    )
}

@Composable
fun TilingSummary(summary: String) {
    val modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 16.dp, vertical = 8.dp)
    Card(
        modifier = modifier,
    ) {
        Text(
            modifier = modifier,
            text = summary
        )
    }
}

internal fun Int.pageRange(itemsPerPage: Int): IntRange {
    val start = this * itemsPerPage
    return start.until(start + itemsPerPage)
}

fun PageQuery.colorShiftingTiles(itemsPerPage: Int, isDark: Boolean) =
    percentageAndIndex().map { (percentage, count) ->
        page.pageRange(itemsPerPage).mapIndexed { index, number ->
            NumberTile(
                number = number,
                color = interpolateColors(
                    fraction = percentage,
                    startValue = MutedColors.colorAt(
                        isDark = isDark,
                        index = index + count
                    ),
                    endValue = MutedColors.colorAt(
                        isDark = isDark,
                        index = index + count + 1
                    )
                ),
            )
        }
    }
        .map { if (isAscending) it else it.asReversed() }

private fun percentageAndIndex(
    changeDelayMillis: Long = 200L
): Flow<Pair<Float, Int>> = flow {
    var percentage = 0f
    var index = 0

    while (true) {
        percentage += 0.05f
        if (percentage > 1f) {
            percentage = 0f
            index++
        }

        emit(percentage to index)
        delay(changeDelayMillis)
    }
}

object MutedColors {
    private val mutedColors = intArrayOf(
        Color(0xFF2980b9).toArgb(), // Belize Hole
        Color(0xFF2c3e50).toArgb(), // Midnight Blue
        Color(0xFFc0392b).toArgb(), // Pomegranate
        Color(0xFF16a085).toArgb(), // Green Sea
        Color(0xFF7f8c8d).toArgb() // Concrete
    )

    private val darkerMutedColors = intArrayOf(
        Color(0xFF304233).toArgb(),
        Color(0xFF353b45).toArgb(),
        Color(0xFF392e3a).toArgb(),
        Color(0xFF3e2a2a).toArgb(),
        Color(0xFF474747).toArgb()
    )

    fun colorAt(isDark: Boolean, index: Int) = palette(isDark).circular(index)

    fun random(isDark: Boolean): Int = palette(isDark).random()

    private fun palette(isDark: Boolean): IntArray = when (isDark) {
        true -> mutedColors
        else -> darkerMutedColors
    }
}

private fun IntArray.circular(index: Int) = this[index % size]

private fun IntArray.random() = this[(Random.Default.nextInt(size))]

fun interpolateColors(fraction: Float, startValue: Int, endValue: Int): Int {
    val startA = (startValue shr 24 and 0xff) / 255.0f
    var startR = (startValue shr 16 and 0xff) / 255.0f
    var startG = (startValue shr 8 and 0xff) / 255.0f
    var startB = (startValue and 0xff) / 255.0f
    val endA = (endValue shr 24 and 0xff) / 255.0f
    var endR = (endValue shr 16 and 0xff) / 255.0f
    var endG = (endValue shr 8 and 0xff) / 255.0f
    var endB = (endValue and 0xff) / 255.0f

    // convert from sRGB to linear
    startR = startR.toDouble().pow(2.2).toFloat()
    startG = startG.toDouble().pow(2.2).toFloat()
    startB = startB.toDouble().pow(2.2).toFloat()
    endR = endR.toDouble().pow(2.2).toFloat()
    endG = endG.toDouble().pow(2.2).toFloat()
    endB = endB.toDouble().pow(2.2).toFloat()

    // compute the interpolated color in linear space
    var a = startA + fraction * (endA - startA)
    var r = startR + fraction * (endR - startR)
    var g = startG + fraction * (endG - startG)
    var b = startB + fraction * (endB - startB)

    // convert back to sRGB in the [0..255] range
    a *= 255.0f
    r = r.toDouble().pow(1.0 / 2.2).toFloat() * 255.0f
    g = g.toDouble().pow(1.0 / 2.2).toFloat() * 255.0f
    b = b.toDouble().pow(1.0 / 2.2).toFloat() * 255.0f

    return a.roundToInt() shl 24 or (r.roundToInt() shl 16) or (g.roundToInt() shl 8) or b.roundToInt()
}